package com.ims.metnum.lineal;

import java.util.ArrayList;
import java.util.List;

import com.ims.metnum.lineal.exceptions.EndConditionIsLessThanOrEqualZero;
import com.ims.metnum.lineal.exceptions.EndConditionIsLessThanSignificantDigitsException;
import com.ims.metnum.lineal.exceptions.MatrixDimensionsException;
import com.ims.metnum.lineal.exceptions.MatrixNotSquareException;
import com.ims.metnum.lineal.exceptions.MaximumNumberOfIterationsLessThan1Exception;
import com.ims.metnum.lineal.exceptions.MethodIsNotConvergentException;
import com.ims.metnum.utils.Matrix;
import com.ims.metnum.utils.RoundDecimal;

/**
 * Numeric Methods <b>Jacobi<b>. Linear Ecuations<br>
 * AX=B; A=L+D+U; A = matrix nxn 
 * @author Luciano 
 * 
 * @author Luciano de Asteinza
 */
public class Jacobi extends IterativeNumericalMethod {
		
	double[][] matrixA;
	double[] matrixB, matrixX0, matrixXn;
	List<double[]> iterationsResults;
	int iterationNumber;
	double endCondition;
	int maxIterationNumber;
	boolean convergent;
	int significantDigits;

	/**
	 * 
	 * @param matrixA the coefficient matrix
	 * @param matrixB the matrix of independent terms
	 * @param matrixX0 initial guess to the solution
	 * @param endCondition
	 * @param maxIterationNumber
	 * @throws MatrixNotSquareException if the matrix is not square
	 * @throws MatrixDimensionsException if dimensions of the matrix of initials solutions or the matrix of independent terms do not correspond to the number of unknowns 
	 * @throws MethodIsNotConvergentException if a element of the main diagonal of matrixA equals 0
	 * @throws MaximumNumberOfIterationsLessThan1Exception
	 * @throws EndConditionIsLessThanOrEqualZero
	 */
	public Jacobi(double[][] matrixA, double[] matrixB, double[] matrixX0, double endCondition, int maxIterationNumber)
			throws MatrixNotSquareException, MatrixDimensionsException, MethodIsNotConvergentException, MaximumNumberOfIterationsLessThan1Exception, EndConditionIsLessThanOrEqualZero {		
		
		Matrix objectMatrixA = new Matrix(matrixA, matrixB);
		if(endCondition <=0 ){
			throw new EndConditionIsLessThanOrEqualZero("Jacobi: end condition = " + endCondition);
		}if(maxIterationNumber<1){
			throw new MaximumNumberOfIterationsLessThan1Exception("Jacobi: max number of iterations = " + maxIterationNumber);
		}else if (!objectMatrixA.isSquareMatrix()) {
			throw new MatrixNotSquareException("Matrix is not square.");
		} else if (matrixA.length != matrixX0.length) {
			throw new MatrixDimensionsException(
					"The dimension of the coefficient matrix does not match the dimension of the matrix of solutions");
		} else {
			this.maxIterationNumber = maxIterationNumber;
			this.iterationsResults = new ArrayList<double[]>();
			this.significantDigits = -1;
			this.matrixA = objectMatrixA.getMatrixC();
			this.matrixB = objectMatrixA.getMatrixB();
			this.matrixX0 = matrixX0.clone();
			this.matrixXn = new double[this.matrixA.length];

			this.endCondition = endCondition;
			
			
			for (int i = 0; i < matrixA.length; i++) {
				this.matrixXn[i] = 0;
			}
			iterationsResults.add(matrixX0);
			iterationNumber = 0;
			
			if(satisfiesDivergenceCondition()){
				throw new MethodIsNotConvergentException("The method is not convergent with the values ​​currently entered, main diagonal element equals zero");
			}			
		}
	}

	/**
	 * 
	 * @param matrixA
	 * @param matrixB
	 * @param matrixX0
	 * @param endCondition
	 * @param maxIterationNumber
	 * @param significantDigits
	 * @throws MatrixNotSquareException
	 * @throws MatrixDimensionsException
	 * @throws MethodIsNotConvergentException
	 * @throws MaximumNumberOfIterationsLessThan1Exception
	 * @throws EndConditionIsLessThanOrEqualZero
	 * @throws EndConditionIsLessThanSignificantDigitsException
	 */
	public Jacobi(double[][] matrixA, double[] matrixB, double[] matrixX0, double endCondition, int maxIterationNumber, int significantDigits)
			throws MatrixNotSquareException, MatrixDimensionsException,
			MethodIsNotConvergentException,
			MaximumNumberOfIterationsLessThan1Exception,
			EndConditionIsLessThanOrEqualZero, EndConditionIsLessThanSignificantDigitsException {

		Matrix objectMatrixA = new Matrix(matrixA, matrixB);
		if (endCondition <= 0) {
			throw new EndConditionIsLessThanOrEqualZero(
					"Jacobi: end condition = " + endCondition);
		}
		if (maxIterationNumber < 1) {
			throw new MaximumNumberOfIterationsLessThan1Exception(
					"Jacobi: max number of iterations = " + maxIterationNumber);
		} else if (!objectMatrixA.isSquareMatrix()) {
			throw new MatrixNotSquareException("Matrix is not square.");
		} else if (matrixA.length != matrixX0.length) {
			throw new MatrixDimensionsException(
					"The dimension of the coefficient matrix does not match the dimension of the matrix of solutions");
		} else {
			this.maxIterationNumber = maxIterationNumber;
			this.iterationsResults = new ArrayList<double[]>();
			this.significantDigits = significantDigits;
			this.matrixA = objectMatrixA.getMatrixC();
			this.matrixB = objectMatrixA.getMatrixB();
			this.matrixX0 = matrixX0.clone();
			this.matrixXn = new double[this.matrixA.length];

			this.endCondition = endCondition;

			for (int i = 0; i < matrixA.length; i++) {
				this.matrixXn[i] = 0;
			}
			iterationsResults.add(matrixX0);
			iterationNumber = 0;

			if (satisfiesDivergenceCondition()) {
				throw new MethodIsNotConvergentException(
						"The method is not convergent with the values ​​currently entered, main diagonal element equals zero");
			}
			
			if(endCondition<(1/Math.pow(10, significantDigits))){
				throw new EndConditionIsLessThanSignificantDigitsException("The end condition number does not match with significant digits number");
			}
			
			this.roundAllMatrixs();
		}
	}
	
	private void roundAllMatrixs(){
		if(significantDigits>=0){
			for(int i=0; i<this.matrixA.length; i++){
				for(int j=0; j<this.matrixA[i].length; j++){
						this.matrixA[i][j]  = RoundDecimal.roundDouble(this.matrixA[i][j], significantDigits);
				}
				this.matrixB[i] = RoundDecimal.roundDouble(this.matrixB[i], significantDigits);
				this.matrixX0[i] = RoundDecimal.roundDouble(this.matrixX0[i], significantDigits);			
			}
		}
		
		
	}
	
	public double[][] getMatrixA() {
		return matrixA;
	}
	
	public double[] getMatrixB() {
		return matrixB;
	}
	
	public double[] getMatrixX0() {
		return matrixX0;
	}



	public double[] getMatrixXn() {
		return matrixXn;
	}



	public List<double[]> getIterationsResults() {
		return iterationsResults;
	}



	public int getIterationNumber() {
		return iterationNumber;
	}



	public double getEndCondition() {
		return endCondition;
	}

	public int getMaxIterationNumber() {
		return maxIterationNumber;
	}

	public int getSignificantDigits() {
		return significantDigits;
	}

	/**
	 * run the method until it meets the condition of completion or exceeding the maximum number of iterations
	 */
	@Override
	public void run() {
		while (iterationNumber < maxIterationNumber && !convergent ){
			iterate();
		}		
	}

	
	@Override
	public boolean isConvergent() {
		return convergent;
	}
	
	/**
 	 * main diagonal element equals zero
 	 */
	@Override
	public boolean satisfiesDivergenceCondition() {
		// TODO Auto-generated method stub
		boolean isDivergent = false; 
		for(int i = 0; i<matrixA.length; i++){
			if(matrixA[i][i] == 0){
				isDivergent = true;
				break;
			}
		}
		return isDivergent;
	}

	/**
	 * Executes a method of iteration
	 */			
	@Override
	public void iterate() {
		double sum = 0;
		matrixXn = new double[matrixA.length];
		for(int i =0; i<matrixA.length; i++){
			sum=0;
			for(int j = 0; j< matrixA.length; j++){
				if(i!=j){
					sum += matrixA[i][j] * iterationsResults.get(iterationNumber)[j];
				}
			}
			matrixXn[i] = ( matrixB[i] - sum ) / matrixA[i][i];
			if(significantDigits>=0){
				this.matrixXn[i] = RoundDecimal.roundDouble(matrixXn[i], significantDigits);
			}
		}
		iterationsResults.add(matrixXn);
		iterationNumber++;
		checkConvergence();
		
	}
	
	private void checkConvergence(){
		int count = 0;
		for (int i = 0 ; i<matrixXn.length; i++){
			double xn = iterationsResults.get(iterationNumber)[i];
			double xnless1 = iterationsResults.get(iterationNumber- 1 )[i];
			if( Math.abs(xn - xnless1) < endCondition){
				count++;
			}			
		}
		convergent = count < matrixXn.length ? false : true;
	}

	@Override
	public boolean satisfiesConvergenceCondition() {
		Matrix matrixC = new Matrix(matrixA);
		return matrixC.isDiagonallyDominant();
	}
	
	

}
